﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LightCAD.MathLib;
using LightCAD.Three.OpenGL;
using static LightCAD.Three.TypeUtils;

namespace LightCAD.Three
{
    public class WebGLBindingState
    {
        public int geometry;
        public int program;
        public bool wireframe;
        public ListEx<int> newAttributes;
        public ListEx<int> enabledAttributes;
        public ListEx<int> attributeDivisors;
        public int _object;
        public JsObj<WebGLAttributeData> attributes;
        public BufferAttribute index;
        public int attributesNum;
    }
    public class WebGLAttributeData
    {
        public object data;
        public BufferAttribute attribute;
    }
    public class WebGLBindingStates : IDispose
    {
        public readonly WebGLExtensions extensions;
        public readonly WebGLAttributes attributes;
        public readonly WebGLCapabilities capabilities;
        public readonly int maxVertexAttributes;
        public readonly int extension;

        public readonly bool vaoAvailable;
        //geometryId||programId,wireframe,bindingState
        public readonly JsObj<int, JsObj<int, JsObj<bool, WebGLBindingState>>> bindingStates;
        public readonly WebGLBindingState defaultState;
        public WebGLBindingState currentState;
        public bool forceUpdate;

        public WebGLBindingStates(WebGLExtensions extensions, WebGLAttributes attributes, WebGLCapabilities capabilities)
        {
            this.extensions = extensions;
            this.attributes = attributes;
            this.capabilities = capabilities;
            this.maxVertexAttributes = (int)gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
            this.extension = capabilities.isWebGL2 ? -1 : extensions.get("OES_vertex_array_object");
            this.vaoAvailable = capabilities.isWebGL2 || extension != -1;
            this.bindingStates = new JsObj<int, JsObj<int, JsObj<bool, WebGLBindingState>>>();
            this.defaultState = createBindingState();
            this.currentState = defaultState;
            this.forceUpdate = false;
        }

        public void setup(Object3D _object, Material material, WebGLProgram program, BufferGeometry geometry, BufferAttribute index)
        {
            var updateBuffers = false;

            if (vaoAvailable)
            {
                var state = getBindingState(geometry, program, material);
                if (currentState != state)
                {
                    currentState = state;
                    bindVertexArrayObject(currentState._object);
                }

                updateBuffers = needsUpdate(_object, geometry, program, index);

                if (updateBuffers) saveCache(_object, geometry, program, index);

            }
            else
            {

                var wireframe = (material.wireframe);

                if (currentState.geometry != geometry.id ||
                    currentState.program != program.id ||
                    currentState.wireframe != wireframe)
                {

                    currentState.geometry = geometry.id;
                    currentState.program = program.id;
                    currentState.wireframe = wireframe;

                    updateBuffers = true;

                }

            }

            if (index != null)
            {

                attributes.update(index, gl.ELEMENT_ARRAY_BUFFER);

            }

            if (updateBuffers || forceUpdate)
            {

                forceUpdate = false;

                setupVertexAttributes(_object, material, program, geometry);

                if (index != null)
                {

                    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, attributes.get(index).buffer);

                }

            }

        }

        public int createVertexArrayObject()
        {
            gl.GenVertexArrays(1, out int vao);
            return vao;
            //if (capabilities.isWebGL2) return gl.createVertexArray();
            //return extension.createVertexArrayOES();
        }

        public void bindVertexArrayObject(int vao)
        {
            gl.BindVertexArray(vao);
        }

        public void deleteVertexArrayObject(int vao)
        {

            gl.DeleteVertexArray(vao);
        }

        public WebGLBindingState getBindingState(BufferGeometry geometry, WebGLProgram program, Material material)
        {

            var wireframe = (material.wireframe);

            var programMap = bindingStates[geometry.id];

            if (programMap == null)
            {

                programMap = new JsObj<int, JsObj<bool, WebGLBindingState>>();
                bindingStates[geometry.id] = programMap;

            }

            var stateMap = programMap[program.id];

            if (stateMap == null)
            {

                stateMap = new JsObj<bool, WebGLBindingState>();
                programMap[program.id] = stateMap;

            }

            var state = stateMap[wireframe];

            if (state == null)
            {

                state = createBindingState(createVertexArrayObject());
                stateMap[wireframe] = state;

            }

            return state;

        }

        public WebGLBindingState createBindingState(int vao = 0)
        {

            var newAttributes = new ListEx<int>();
            var enabledAttributes = new ListEx<int>();
            var attributeDivisors = new ListEx<int>();
            for (var i = 0; i < maxVertexAttributes; i++)
            {
                newAttributes[i] = 0;
                enabledAttributes[i] = 0;
                attributeDivisors[i] = 0;
            }

            return new WebGLBindingState
            {

                // for backward compatibility on non-VAO support browser
                geometry = 0,
                program = 0,
                wireframe = false,

                newAttributes = newAttributes,
                enabledAttributes = enabledAttributes,
                attributeDivisors = attributeDivisors,
                _object = vao,
                attributes = new JsObj<WebGLAttributeData>(),
                index = null
            };
        }

        public bool needsUpdate(Object3D _object, BufferGeometry geometry, WebGLProgram program, BufferAttribute index)
        {

            var cachedAttributes = currentState.attributes;
            var geometryAttributes = geometry.attributes;
            var attributesNum = 0;

            var programAttributes = program.getAttributes();

            foreach (var item in programAttributes)
            {
                var name = item.Key;
                var programAttribute = programAttributes[name];

                if (programAttribute.location >= 0)
                {

                    var cachedAttribute = cachedAttributes[name];
                    var geometryAttribute = geometryAttributes[name];
                    if (geometryAttribute == null)
                    {

                        if (name == "instanceMatrix" && (_object as InstancedMesh).instanceMatrix != null)
                            geometryAttribute = (_object as InstancedMesh).instanceMatrix;
                        if (name == "instanceColor" && (_object as InstancedMesh).instanceColor != null)
                            geometryAttribute = (_object as InstancedMesh).instanceColor;

                    }

                    if (cachedAttribute == null)
                        return true;

                    if (cachedAttribute.attribute != geometryAttribute)
                        return true;

                    if (geometryAttribute != null && cachedAttribute.data != (geometryAttribute as InterleavedBufferAttribute)?.data)
                        return true;

                    attributesNum++;
                }
            }

            if (currentState.attributesNum != attributesNum)
                return true;

            if (currentState.index != index)
                return true;

            return false;

        }

        public void saveCache(Object3D _object, BufferGeometry geometry, WebGLProgram program, BufferAttribute index)
        {

            var cache = new JsObj<WebGLAttributeData>();
            var attributes = geometry.attributes;
            var attributesNum = 0;

            var programAttributes = program.getAttributes();

            foreach (var item in programAttributes)
            {
                var name = item.Key;
                var programAttribute = programAttributes[name];

                if (programAttribute.location >= 0)
                {

                    var attribute = attributes[name];
                    if (attribute == null)
                    {

                        if (name == "instanceMatrix" && (_object as InstancedMesh).instanceMatrix != null)
                            attribute = (_object as InstancedMesh).instanceMatrix;
                        if (name == "instanceColor" && (_object as InstancedMesh).instanceColor != null)
                            attribute = (_object as InstancedMesh).instanceColor;
                    }

                    var data = new WebGLAttributeData();
                    data.attribute = attribute;

                    if (attribute != null && attribute is InterleavedBufferAttribute)//attribute.data!=null)
                    {
                        data.data = (attribute as InterleavedBufferAttribute).data;
                    }
                    cache[name] = data;
                    attributesNum++;

                }

            }

            currentState.attributes = cache;
            currentState.attributesNum = attributesNum;
            currentState.index = index;

        }

        public void initAttributes()
        {

            var newAttributes = currentState.newAttributes;

            for (int i = 0, il = newAttributes.Length; i < il; i++)
            {
                newAttributes[i] = 0;
            }

        }

        public void enableAttribute(int attribute)
        {

            enableAttributeAndDivisor(attribute, 0);

        }

        public void enableAttributeAndDivisor(int attribute, int meshPerAttribute)
        {

            var newAttributes = currentState.newAttributes;
            var enabledAttributes = currentState.enabledAttributes;
            var attributeDivisors = currentState.attributeDivisors;
            newAttributes[attribute] = 1;

            if (enabledAttributes[attribute] == 0)
            {

                gl.enableVertexAttribArray(attribute);
                enabledAttributes[attribute] = 1;

            }

            if (attributeDivisors[attribute] != meshPerAttribute)
            {
                gl.vertexAttribDivisor(attribute, meshPerAttribute);
            }

        }

        public void disableUnusedAttributes()
        {

            var newAttributes = currentState.newAttributes;
            var enabledAttributes = currentState.enabledAttributes;

            for (int i = 0, il = enabledAttributes.Length; i < il; i++)
            {

                if (enabledAttributes[i] != newAttributes[i])
                {

                    gl.disableVertexAttribArray(i);
                    enabledAttributes[i] = 0;
                }
            }
        }

        public void vertexAttribPointer(int index, int size, int type, bool normalized, int stride, int offset)
        {
            if (capabilities.isWebGL2 && (type == gl.INT || type == gl.UNSIGNED_INT))
            {
                gl.vertexAttribIPointer(index, size, type, stride, offset);
            }
            else
            {
                gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
            }
        }

        public void setupVertexAttributes(Object3D _object, Material material, WebGLProgram program, BufferGeometry geometry)
        {

            if (capabilities.isWebGL2 == false && (_object is InstancedMesh || geometry is InstancedBufferGeometry))
            {

                //if (extensions.get("ANGLE_instanced_arrays") == null) return;

            }

            initAttributes();

            var geometryAttributes = geometry.attributes;

            var programAttributes = program.getAttributes();

            var materialDefaultAttributeValues = (material as ShaderMaterial)?.defaultAttributeValues;

            foreach (var item in programAttributes)
            {
                var name = item.Key;
                var programAttribute = programAttributes[name];

                if (programAttribute.location >= 0)
                {

                    var geometryAttribute = geometryAttributes[name];
                    if (geometryAttribute == null)
                    {

                        if (name == "instanceMatrix" && (_object as InstancedMesh).instanceMatrix != null) geometryAttribute = (_object as InstancedMesh).instanceMatrix;
                        if (name == "instanceColor" && (_object as InstancedMesh).instanceColor != null) geometryAttribute = (_object as InstancedMesh).instanceColor;

                    }

                    if (geometryAttribute != null)
                    {

                        var normalized = geometryAttribute.normalized;
                        var size = geometryAttribute.itemSize;

                        var attribute = attributes.get(geometryAttribute);

                        // TODO Attribute may not be available on context restore

                        if (attribute == null) continue;

                        var buffer = attribute.buffer;
                        var type = attribute.type;
                        var bytesPerElement = attribute.bytesPerElement;

                        if (geometryAttribute is InterleavedBufferAttribute)
                        {
                            var interleadvedAttr = geometryAttribute as InterleavedBufferAttribute;
                            var data = interleadvedAttr.data;
                            var stride = data.stride;
                            var offset = interleadvedAttr.offset;

                            if (data is InstancedInterleavedBuffer)
                            {
                                var instancedData = data as InstancedInterleavedBuffer;
                                for (var i = 0; i < programAttribute.locationSize; i++)
                                {

                                    enableAttributeAndDivisor(programAttribute.location + i, instancedData.meshPerAttribute);

                                }

                                if (_object is InstancedMesh != true && (geometry as InstancedBufferGeometry)._maxInstanceCount == 0)
                                {

                                    (geometry as InstancedBufferGeometry)._maxInstanceCount = instancedData.meshPerAttribute * data.count;

                                }

                            }
                            else
                            {

                                for (var i = 0; i < programAttribute.locationSize; i++)
                                {

                                    enableAttribute(programAttribute.location + i);

                                }

                            }

                            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

                            for (var i = 0; i < programAttribute.locationSize; i++)
                            {

                                vertexAttribPointer(
                                    programAttribute.location + i,
                                    size / programAttribute.locationSize,
                                type,
                                normalized,
                                stride * bytesPerElement,
                                    (offset + (size / programAttribute.locationSize) * i) * bytesPerElement
                                );

                            }

                        }
                        else
                        {

                            if (geometryAttribute is InstancedBufferAttribute)
                            {
                                var instancedAttr = geometryAttribute as InstancedBufferAttribute;
                                for (var i = 0; i < programAttribute.locationSize; i++)
                                {

                                    enableAttributeAndDivisor(programAttribute.location + i, instancedAttr.meshPerAttribute);

                                }

                                if (_object is InstancedMesh != true && (geometry as InstancedBufferGeometry)._maxInstanceCount == 0)
                                {

                                    (geometry as InstancedBufferGeometry)._maxInstanceCount = (geometryAttribute as InstancedBufferAttribute).meshPerAttribute * geometryAttribute.count;

                                }

                            }
                            else
                            {

                                for (var i = 0; i < programAttribute.locationSize; i++)
                                {

                                    enableAttribute(programAttribute.location + i);

                                }

                            }

                            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

                            for (var i = 0; i < programAttribute.locationSize; i++)
                            {

                                vertexAttribPointer(
                                    programAttribute.location + i,
                                    size / programAttribute.locationSize,
                                type,
                                    normalized,
                                size * bytesPerElement,
                                    (size / programAttribute.locationSize) * i * bytesPerElement
                                );

                            }

                        }

                    }
                    else if (materialDefaultAttributeValues != null)
                    {
                        var value = materialDefaultAttributeValues[name];

                        if (value != null)
                        {

                            switch (value.Length)
                            {

                                case 2:
                                    gl.vertexAttrib2fv(programAttribute.location, toFloat32Array(value));
                                    break;

                                case 3:
                                    gl.vertexAttrib3fv(programAttribute.location, toFloat32Array(value));
                                    break;

                                case 4:
                                    gl.vertexAttrib4fv(programAttribute.location, toFloat32Array(value));
                                    break;

                                default:
                                    gl.vertexAttrib1fv(programAttribute.location, toFloat32Array(value));
                                    break;
                            }
                        }
                    }
                }
            }
            disableUnusedAttributes();
        }

        public void dispose()
        {
            reset();
            var bkeys = this.bindingStates.Keys.ToArray();
            foreach (var geometryId in bkeys)
            {

                var programMap = this.bindingStates[geometryId];

                var keys = programMap.Keys.ToArray();
                foreach (var programId in keys)
                {
                    var stateMap = programMap[programId];

                    var smKeys = stateMap.Keys.ToArray();
                    foreach (var wireframe in smKeys)
                    {
                        deleteVertexArrayObject(stateMap[wireframe]._object);

                        stateMap.remove(wireframe);
                        //devare stateMap[wireframe];

                    }
                    //devare programMap[programId];
                    programMap.remove(programId);

                }
                bindingStates.remove(geometryId);
                //devare bindingStates[geometryId];

            }

        }

        public void releaseStatesOfGeometry(BufferGeometry geometry)
        {

            if (this.bindingStates[geometry.id] == null) return;

            var programMap = this.bindingStates[geometry.id];
            var programMapKeys = programMap.Keys.ToList();
            foreach (var programId in programMapKeys)
            {
                var stateMap = programMap[programId];
                var stateMapKeys = stateMap.Keys.ToList();
                foreach (var wireframe in stateMapKeys)
                {
                    deleteVertexArrayObject(stateMap[wireframe]._object);
                    stateMap.remove(wireframe);
                }
                programMap.remove(programId);
                //devare programMap[programId];
            }
            bindingStates.remove(geometry.id);
            //devare bindingStates[geometry.id];

        }

        public void releaseStatesOfProgram(WebGLProgram program)
        {

            foreach (var item in this.bindingStates)
            {
                var geometryId = item.Key;
                var programMap = this.bindingStates[geometryId];

                if (programMap[program.id] == null) continue;

                var stateMap = programMap[program.id];

                var keys = stateMap.Keys.ToList();
                foreach (var wireframe in keys)
                {
                    deleteVertexArrayObject(stateMap[wireframe]._object);
                    stateMap.remove(wireframe);
                    //devare stateMap[wireframe];
                }
                programMap.remove(program.id);
                //devare programMap[program.id];

            }

        }

        public void reset()
        {

            resetDefaultState();
            forceUpdate = true;

            if (currentState == defaultState) return;

            currentState = defaultState;
            bindVertexArrayObject(currentState._object);

        }

        // for backward-compatibility

        public void resetDefaultState()
        {

            defaultState.geometry = -1;
            defaultState.program = -1;
            defaultState.wireframe = false;
        }
    };
}
